/*****************************************************************************
 * Copyright (C) Stacy Curl. All rights reserved.                            *
 * ------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the BSD      *
 * style license a copy of which has been included with this distribution in *
 * the LICENSE.txt file.                                                     *
 *****************************************************************************/
package picounit.features.base;


import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

// TODO: Consider merging this into StubFactory, 'getConstructorToUse' really means get constructor
//		to use for stubbing
class ConstructorUtil {
	private final StubFactory stubFactory;

	public ConstructorUtil(StubFactory stubFactory) {
		this.stubFactory = stubFactory;
	}

	public <T> Constructor<T> getConstructorToUse(Class<T> clazz) {
		// First try to use the default constructor
		try {
			Constructor<T> declaredConstructor = clazz.getDeclaredConstructor(new Class[0]);

			if (!isPrivate(declaredConstructor)) {
				return declaredConstructor;
			}
		}
		catch (NoSuchMethodException e) {
		}

		Constructor<T> nonPrimativeConstructor = null;

		for (Constructor<T> declaredConstructor : clazz.getDeclaredConstructors()) {
			if (isPrivate(declaredConstructor)) {
				continue;
			}

			List<Class<?>> parameterTypes = Arrays.asList(declaredConstructor.getParameterTypes());

			if (referencesDeclaringClass(declaredConstructor, parameterTypes)) {
				continue;
			}

			if (containsOnlySimpleTypes(parameterTypes)) {
				return declaredConstructor;
			}
			else if (nonPrimativeConstructor == null) {
				nonPrimativeConstructor = declaredConstructor;
			}
		}

		if (nonPrimativeConstructor != null) {
			return nonPrimativeConstructor;
		}

		throw new IllegalArgumentException("No visible constructors in class " + clazz.getName());
	}

	// TODO: Consider what will happen if a constructor requires an interface or abstract class, need
	//		to create dynamic proxy or cglib
	public Object[] getArgsForTypes(Class<?> aClass, Class<?>[] methodTypes) throws IllegalArgumentException,
		InstantiationException, IllegalAccessException, InvocationTargetException {

		List<Object> methodArgs = new ArrayList<Object>(methodTypes.length);

		try {
			for (Class<?> methodType : methodTypes) {
				if (Modifier.isFinal(methodType.getModifiers()) && !methodType.isPrimitive()) {
					methodArgs.add(instantiateFinalClass(methodType));
				}
				else {
					methodArgs.add(stubFactory.create(methodType));
				}
			}
		}
		catch (IllegalArgumentException illegalArgumentException) {
			throw new IllegalArgumentException(aClass.getName() + " - " +
				Arrays.asList(methodTypes).toString(), illegalArgumentException);
		}

		return methodArgs.toArray();
	}

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Implementation methods
	///////////////////////////////////////////////////////////////////////////////////////////////

	@SuppressWarnings("unchecked")
	private <T> T instantiateFinalClass(Class<T> finalClass)
		throws InstantiationException, IllegalAccessException, InvocationTargetException {

		if (uninstantiableTypeMap.containsKey(finalClass)) {
			return (T) uninstantiableTypeMap.get(finalClass);
		}

		Constructor<T> argConstructor = getConstructorToUse(finalClass);

		Object[] argsForTypes = getArgsForTypes(finalClass, argConstructor.getParameterTypes());

		return argConstructor.newInstance(argsForTypes);
	}

	private boolean containsOnlySimpleTypes(List<Class<?>> parameterTypes) {
		return simpleTypes.containsAll(parameterTypes);
	}
	
	private boolean referencesDeclaringClass(Constructor<?> declaredConstructor,
		List<Class<?>> parameterTypes) {

		return parameterTypes.contains(declaredConstructor.getDeclaringClass());
	}

	private boolean isPrivate(Constructor<?> declaredConstructor) {
		return Modifier.isPrivate(declaredConstructor.getModifiers());
	}
	
	private final Set<Class<?>> simpleTypes = simpleTypes();

	private Set<Class<?>> simpleTypes() {
		Set<Class<?>> simpleTypes = new HashSet<Class<?>>();

		simpleTypes.add(boolean.class);
		simpleTypes.add(byte.class);
		simpleTypes.add(char.class);
		simpleTypes.add(double.class);
		simpleTypes.add(float.class);
		simpleTypes.add(int.class);
		simpleTypes.add(long.class);
		simpleTypes.add(short.class);
		simpleTypes.add(Boolean.class);
		simpleTypes.add(Byte.class);
		simpleTypes.add(Character.class);
		simpleTypes.add(Double.class);
		simpleTypes.add(Float.class);
		simpleTypes.add(Integer.class);
		simpleTypes.add(Long.class);
		simpleTypes.add(Short.class);
		simpleTypes.add(String.class);
		simpleTypes.add(Object.class);

		return simpleTypes;
	}
	
	private final Map<Class<?>, Object> uninstantiableTypeMap = uninstantiableTypes();

	private Map<Class<?>, Object> uninstantiableTypes() {
		Map<Class<?>, Object> uninstantiableTypeMap  = new HashMap<Class<?>, Object>();

		uninstantiableTypeMap.put(Class.class, Class.class);
		uninstantiableTypeMap.put(Constructor.class, Object.class.getConstructors()[0]);
		uninstantiableTypeMap.put(Method.class, Object.class.getMethods()[0]);

		return uninstantiableTypeMap;
	}
}
